
package at.fhj.ase.crypto.common.util;

import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class providing useful cryptographic operations like signing a session key, or creating a public/private key pair.
 * <p>
 * The class uses a logger instance and initiates a md5-messagedigest.<br>
 * If initialization fails, an {@link IllegalStateException} is thrown.
 * <p>
 * The class also helds some keys like {@link #RSA} which should optimize readability and which could also be easily exported into a configuration file.
 * 
 * @author sfe | www.fh-joanneum.at | Practical Aspects of Cryptography
 */
public final class CryptoUtil {
  /** The sole logging instance */
  private final static Logger logger = LoggerFactory.getLogger(CryptoUtil.class);
  /** The md5 message digest that is created by the static initializer block */
  private final static MessageDigest md5MessageDigest;

  /** The asymmetric encryption algorithm "RSA" as a {@link String} representation */
  public static final String RSA = "RSA";

  /** The symmetric encryption algorithm "AES" as a {@link String} representation */
  public static final String AES = "AES";

  /** The non-reverse hash operation "MD5" as a {@link String} representation */
  public static final String MD5 = "MD5";

  /** The signature mode "MD5 with RSA" */
  public static final String MD5_RSA = "MD5withRSA";

  static {
    try {
      // we have to do that in a static-block as it throws checked exception -> convert it into RuntimeException
      md5MessageDigest = MessageDigest.getInstance(MD5);
    } catch (NoSuchAlgorithmException e) {
      // should never be the case, but to be sure...
      logger.error("Could not instantiate MD5 message digest", e);
      throw createRuntimeException(e);
    }
  }

  private static IllegalStateException createRuntimeException(Exception e) {
    return new IllegalStateException(e);
  }

  private CryptoUtil() {
    throw new AssertionError("EncryptorUtil: Util classes are not intended/allowed to be instantiated!");
  }

  /**
   * @param data the bytes that shall be hashed with the MD5 algorithm
   * @return the hashed bytes or the provided bytes if an exception occurred.
   */
  public static byte[] toMD5(final byte[] data) {
    md5MessageDigest.update(data);
    return md5MessageDigest.digest();
  }

  /** Convenience method calling {@link CryptoUtil#toMD5(byte[])} after conversion to byte-array */
  public static byte[] toMD5(final String data) {
    return toMD5(data.getBytes());
  }

  /** Convenience method calling {@link CryptoUtil#toMD5(byte[])} after conversion to byte-array */
  public static byte[] toMD5(final char[] data) {
    return toMD5(String.valueOf(data).getBytes());
  }

  /** @return the binary data in a <strong>new</strong> {@link String} instance. */
  public static String toString(final byte[] data) {
    return new String(data);
  }

  /**
   * Method creating a {@link KeyPair} for the specified <code>algorithm</code> and with the specified <code>keySize</code> as<br>
   * initialization size in bit.<br>
   * This key pair contains a public- and a private key.
   * 
   * @param algorithm the algorithm the {@link KeyPairGenerator} shall be initiated with
   * @param keySize the amount of bit the {@link KeyPairGenerator} shall be initiated with
   * @return the generated {@link KeyPair} out of the {@link KeyPairGenerator} with the specified values
   * @throws NoSuchAlgorithmException if the creation of {@link KeyPairGenerator} for the specified algorithm was not possible
   */
  public static KeyPair createKeyPair(final String algorithm, final int keySize) throws NoSuchAlgorithmException {
    KeyPairGenerator kpGen = KeyPairGenerator.getInstance(algorithm);
    kpGen.initialize(keySize);
    return kpGen.generateKeyPair();
  }

  /**
   * Method creating a {@link SecretKey} for the specified <code>algorithm</code> and with the specified <code>keySize</code> as<br>
   * initialization size in bit.<br>
   * This key pair contains a public- and a private key.
   * 
   * @param algorithm the algorithm the {@link KeyGenerator} shall be initiated with
   * @param keySize the amount of bit the {@link KeyGenerator} shall be initiated with
   * @return the generated {@link SecretKey} out of the {@link KeyGenerator} with the specified values
   * @throws NoSuchAlgorithmException if the creation of {@link KeyGenerator} for the specified algorithm was not possible
   */
  public static SecretKey createSecretKey(final String algorithm, final int keySize) throws NoSuchAlgorithmException {
    final KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
    keyGen.init(keySize);
    return keyGen.generateKey();
  }

  /** @return a {@link PublicKey} instance out of the provided byte array using {@link X509EncodedKeySpec} and the {@link KeyFactory} using RSA */
  public static PublicKey getPublicKey(final byte[] publicKey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException,
    InvalidKeyException, IllegalBlockSizeException, BadPaddingException {

    final X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKey);
    return KeyFactory.getInstance(RSA).generatePublic(publicKeySpec);
  }

  /** @return a {@link PrivateKey} instance out of the provided byte array using {@link PKCS8EncodedKeySpec} and the {@link KeyFactory} using RSA */
  public static PrivateKey getPrivateKey(final byte[] privateKey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException,
    InvalidKeyException, IllegalBlockSizeException, BadPaddingException {

    final PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKey);
    return KeyFactory.getInstance(RSA).generatePrivate(privateKeySpec);
  }

  /** @return a byte-array that contains the provided key (also a byte-array) encrypted with the provided <strong>public</strong> key */
  public static byte[] encryptWithPublic(final PublicKey publicKey, final byte[] keyToEncrypt) throws IllegalBlockSizeException, BadPaddingException,
    InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {

    final Cipher cipher = Cipher.getInstance(RSA);
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    return cipher.doFinal(keyToEncrypt);
  }

  /** @return a byte-array that contains the provided key (also a byte-array) decrypted with the provided <strong>private</strong> key */
  public static byte[] decryptWithPrivate(final PrivateKey privateKey, final byte[] keyToEncrypt) throws IllegalBlockSizeException, BadPaddingException,
    InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {

    final Cipher cipher = Cipher.getInstance(RSA);
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    return cipher.doFinal(keyToEncrypt);
  }

  /** @return the provided data array encrypted with the provided {@link SecretKey} */
  public static byte[] encryptWithSecret(final SecretKey secretKey, final byte[] dataToEncrypt) throws NoSuchAlgorithmException, NoSuchPaddingException,
    InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    final Cipher cipher = Cipher.getInstance(AES);
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    return cipher.doFinal(dataToEncrypt);
  }

  /** @return the provided data array decrypted with the provided {@link SecretKey} */
  public static byte[] decryptWithSecret(final SecretKey secretKey, final byte[] dataToDecrypt) throws NoSuchAlgorithmException, NoSuchPaddingException,
    InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    final Cipher cipher = Cipher.getInstance(AES);
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    return cipher.doFinal(dataToDecrypt);
  }

  /** @return the provided key (byte-array) signed with the private key, again as a byte-array */
  public static byte[] signWithPrivateKey(final PrivateKey privateKey, final byte[] keyToSign) throws NoSuchAlgorithmException, InvalidKeyException,
    SignatureException {

    final Signature signature = Signature.getInstance(MD5_RSA);
    signature.initSign(privateKey);
    signature.update(toMD5(keyToSign));
    return signature.sign();
  }

  /**
   * Method checks whether the provided key <code>keyToVerify</code> can be verified using the provided {@link PublicKey}.<br>
   * The verification is done by comparing against the correct provided <code>signKey</code>.
   * 
   * @return <code>true</code> if the <code>keyToVerify</code> can be verified against <code>signKey</code> with the <code>publicKey</code>, otherwise
   * <code>false</code>
   */
  public static boolean verifyWithPublicKey(final PublicKey publicKey, final byte[] signKey, final byte[] keyToVerify) throws NoSuchAlgorithmException,
    InvalidKeyException, SignatureException {

    final Signature signature = Signature.getInstance(MD5_RSA);
    signature.initVerify(publicKey);
    signature.update(toMD5(signKey));
    return signature.verify(keyToVerify);
  }

  /**
   * Get a byte array from a hex string
   * 
   * @param stringHex a string in hex format
   * @return a byte array for the provided hex string
   */
  public static byte[] getBytes(String stringHex) {
    final int len = stringHex.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
      data[i / 2] = (byte) ((Character.digit(stringHex.charAt(i), 16) << 4) + Character.digit(stringHex.charAt(i + 1), 16));
    }
    return data;
  }

  /**
   * Get a Hex String of a byte array.<br>
   * This method is the reverse operation of {@link #getBytes(String)}.
   * 
   * @param bytes the byte array that shall be converted into a hex string
   * @return a hex string representation of a byte array
   */
  public static String getStringHex(byte[] bytes) {
    final StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
      sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
    }
    return sb.toString();
  }
}
